发布于 

Frida 构造数组、对象、Map 和类参数

为什么需要构造复杂参数

在 Android 逆向工程中,我们经常需要 Hook 某些关键函数来分析其行为,或者主动调用目标方法来获取结果。但很多 Java 方法的参数并不是简单的字符串或整数——它们可能是数组、自定义对象、Map、List 甚至枚举类型。如果我们想要在 Frida 脚本中正确调用这些方法,就必须学会如何在 JavaScript 环境中构造出符合 Java 类型系统要求的参数对象。

举个典型的场景:某个加密函数的签名是 encrypt(byte[] key, Map<String, String> params, CipherMode mode)。你要想主动调用这个函数拿到加密结果,就必须:

  1. 构造一个 byte[] 数组作为密钥
  2. 构造一个 HashMap 并填入键值对
  3. 获取或构造一个 CipherMode 枚举值

这就是本文要解决的问题——掌握 Frida 中各种复杂 Java 参数的构造方法。

Frida 中 Java 数组的构造

Frida 提供了 Java.array() 方法来直接在 JavaScript 中创建 Java 数组。这是最常用的数组构造方式。

基本类型数组

// 构造 int 数组
var intArray = Java.array('int', [1, 2, 3, 4, 5]);

// 构造 byte 数组(常用于密钥、加密数据)
var keyBytes = Java.array('byte', [0x41, 0x42, 0x43, 0x44]);

// 构造空数组
var emptyBytes = Java.array('byte', []);

类型签名对照表

Java 类型 Frida 类型字符串
int[] 'int'
byte[] 'byte'
short[] 'short'
long[] 'long'
float[] 'float'
double[] 'double'
boolean[] 'boolean'
char[] 'char'

对象数组

对象数组的构造方式与基本类型类似,但传入的是 Java 对象而非 JavaScript 原始值:

// 构造 String 数组
var StringClass = Java.use('java.lang.String');
var strArray = Java.array('java.lang.String', [
    StringClass.$new("hello"),
    StringClass.$new("world")
]);

// 构造自定义对象数组
var MyClass = Java.use('com.example.MyClass');
var obj1 = MyClass.$new("param1");
var obj2 = MyClass.$new("param2");
var objArray = Java.array('com.example.MyClass', [obj1, obj2]);

从已有 Java 数组复制

如果需要基于一个已有的 Java 数组创建新数组,可以先读取再构造:

// 读取原数组内容
var originalArray = someMethod(); // 返回 byte[]
var length = originalArray.length;
var newArray = [];

for (var i = 0; i < length; i++) {
    newArray.push(originalArray[i]);
}

// 构造新数组
var copy = Java.array('byte', newArray);

Frida 中 Java 对象的构造

构造 Java 对象的核心方法是 $new()。通过 Java.use() 获取类引用后,即可调用其构造函数。

基本构造

// 获取类并实例化
var StringBuilder = Java.use('java.lang.StringBuilder');
var sb = StringBuilder.$new();
sb.append("Hello Frida");
console.log(sb.toString());

带参数的构造

// 带参数构造
var ArrayList = Java.use('java.util.ArrayList');
var list = ArrayList.$new();  // 无参构造
list.add("item1");
list.add("item2");

var File = Java.use('java.io.File');
var file = File.$new("/data/local/tmp/test.txt");  // String 参数构造

调用重载的构造函数

当一个类有多个构造函数时,Frida 会根据参数类型自动匹配。如果自动匹配失败,可以通过 overload 显式指定:

var Integer = Java.use('java.lang.Integer');

// 直接调用,Frida 自动匹配
var intObj1 = Integer.$new(42);

// 显式指定参数类型
var intObj2 = Integer.$new.overload('java.lang.String').$new("42");

Frida 中 String 的处理

java.lang.String 是最常用的类型,但在 Frida 中有一些需要注意的细节。

基本使用

var StringClass = Java.use('java.lang.String');

// 从 JavaScript 字符串创建 Java String
var str = StringClass.$new("Hello World");

// 创建空字符串
var empty = StringClass.$new();

// 从字节数组创建(指定编码)
var bytes = Java.array('byte', [0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD]);
var utf8Str = StringClass.$new.overload('[B', 'java.lang.String').$new(bytes, "UTF-8");

String 类型的 Hook 注意事项

在 Hook String 参数的方法时,直接用 console.log(param) 即可看到字符串内容,因为 Frida 的 RPC 机制会自动处理 String 到 JavaScript 字符串的转换:

var StringClass = Java.use('java.lang.String');
StringClass.getBytes.overload('java.lang.String').implementation = function (charset) {
    var result = this.getBytes(charset);
    console.log("[*] String.getBytes() called: " + this + ", charset: " + charset);
    return result;
};

Frida 中 Map/List 的构造和使用

集合类是 Java 中最常用的参数类型之一,尤其是 API 接口的参数通常以 Map 形式传递。

HashMap 的构造

var HashMap = Java.use('java.util.HashMap');
var map = HashMap.$new();

map.put("appKey", "your_app_key");
map.put("timestamp", String(Date.now()));
map.put("version", "1.0.0");

// 读取值
console.log("appKey = " + map.get("appKey"));

ArrayList 的构造

var ArrayList = Java.use('java.util.ArrayList');
var list = ArrayList.$new();

list.add("element1");
list.add("element2");
list.add("element3");

// 遍历
var iter = list.iterator();
while (iter.hasNext()) {
    console.log(iter.next());
}

// 获取大小
console.log("List size: " + list.size());

使用已有集合构造

有时目标方法要求特定类型的 Map(如 TreeMapLinkedHashMap),需要使用对应的实现类:

var TreeMap = Java.use('java.util.TreeMap');
var sortedMap = TreeMap.$new();
sortedMap.put("c", "value3");
sortedMap.put("a", "value1");
sortedMap.put("b", "value2");
// TreeMap 会自动按键排序: a, b, c

Frida 中枚举类型的构造

枚举在 Android 中广泛使用,例如网络请求的状态码、加密模式等。枚举的构造不是用 $new(),而是通过枚举值字段直接获取。

获取枚举实例

// 假设有如下 Java 枚举:
// public enum CipherMode { AES_CBC, AES_ECB, RSA }

var CipherMode = Java.use('com.example.CipherMode');
var mode = CipherMode.AES_CBC.value;  // 获取枚举实例

// 也可以用 Enum.valueOf 的方式
var Enum = Java.use('java.lang.Enum');
var mode2 = CipherMode.valueOf("AES_ECB");

实际应用

var CryptoUtil = Java.use('com.example.CryptoUtil');

// Hook 构造函数,打印枚举参数
CryptoUtil.$init.overload('[B', 'com.example.CipherMode').implementation = function (key, mode) {
    console.log("[*] CryptoUtil init, mode: " + mode.name());
    return this.$init(key, mode);
};

// 主动调用
var keyBytes = Java.array('byte', [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
                                   0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]);
var cipherMode = Java.use('com.example.CipherMode').AES_CBC.value;
var result = CryptoUtil.encrypt(keyBytes, cipherMode);

Frida 中自定义类的实例化

对于应用自定义的类,需要确保其 ClassLoader 已经加载了该类,否则 Java.use() 会抛出 ClassNotFoundException

使用 ClassFactory 加载

Java.perform(function () {
    // 如果类已经被加载,直接使用
    try {
        var MyCustom = Java.use('com.example.app.MyCustomClass');
        var instance = MyCustom.$new("param");
    } catch (e) {
        console.log("类未加载,尝试枚举 ClassLoader...");
        
        // 遍历 ClassLoader 找到目标类
        Java.enumerateClassLoaders({
            onMatch: function (loader) {
                try {
                    loader.loadClass("com.example.app.MyCustomClass");
                    Java.classFactory.loader = loader;
                    
                    var MyCustom = Java.use('com.example.app.MyCustomClass');
                    var instance = MyCustom.$new("param");
                    console.log("[+] 成功通过 ClassLoader 构造实例");
                } catch (err) {
                    // 此 ClassLoader 中没有该类
                }
            },
            onComplete: function () {
                console.log("[*] ClassLoader 枚举完成");
            }
        });
    }
});

设置默认 ClassFactory

如果在脚本启动时目标类尚未加载,可以在合适的时机(如 Activity.onCreate)切换 ClassFactory:

var Activity = Java.use('android.app.Activity');
Activity.onCreate.implementation = function (savedInstanceState) {
    // 切换到当前 Activity 的 ClassLoader
    Java.classFactory.loader = this.getClassLoader();
    
    // 现在可以安全地使用该 ClassLoader 加载的类了
    var MyCustom = Java.use('com.example.app.MyCustomClass');
    var instance = MyCustom.$new();
    
    return this.onCreate(savedInstanceState);
};

实际案例:Hook 加密函数并构造参数调用

下面通过一个完整的案例,演示如何构造复杂参数并主动调用目标加密方法。

Java.perform(function () {
    // 目标类:com.example.security.Encryptor
    // 目标方法:public static byte[] encrypt(byte[] key, byte[] data, Map<String, String> config)
    
    var Encryptor = Java.use('com.example.security.Encryptor');
    var HashMap = Java.use('java.util.HashMap');
    var StringClass = Java.use('java.lang.String');
    var Base64 = Java.use('android.util.Base64');
    
    // ---- Hook 原始调用,观察参数 ----
    Encryptor.encrypt.implementation = function (key, data, config) {
        console.log("[*] Encryptor.encrypt called");
        console.log("    Key length: " + key.length);
        console.log("    Data length: " + data.length);
        
        // 遍历 config Map
        var keys = config.keySet().iterator();
        while (keys.hasNext()) {
            var k = keys.next();
            console.log("    Config[" + k + "] = " + config.get(k));
        }
        
        var result = this.encrypt(key, data, config);
        console.log("    Result: " + Base64.encodeToString(result, 2));
        return result;
    };
    
    // ---- 主动构造参数调用 ----
    function callEncrypt() {
        // 构造 key (16 字节 AES 密钥)
        var key = Java.array('byte', [
            0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
            0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66
        ]);
        
        // 构造 data (明文数据)
        var plainText = "Hello Frida逆向";
        var dataBytes = plainText.getBytes();
        var data = Java.array('byte', dataBytes);
        
        // 构造 config Map
        var config = HashMap.$new();
        config.put("algorithm", "AES/CBC/PKCS5Padding");
        config.put("iv", "0123456789abcdef");
        config.put("outputFormat", "base64");
        
        // 调用加密方法
        var result = Encryptor.encrypt(key, data, config);
        
        // 将结果转为 Base64 字符串输出
        var encoded = Base64.encodeToString(result, 2);
        console.log("[+] 加密结果: " + encoded);
        return encoded;
    }
    
    // 调用测试
    callEncrypt();
});

实际案例:构造 Map 参数调用签名验证

许多 App 的 API 签名验证函数接收 Map 类型参数,我们需要精确构造才能通过验证:

Java.perform(function () {
    var SignUtil = Java.use('com.example.utils.SignUtil');
    var TreeMap = Java.use('java.util.TreeMap');
    var ArrayList = Java.use('java.util.ArrayList');
    
    // 目标方法:public static String sign(TreeMap<String, String> params, List<String> excludeKeys)
    
    function generateSignature() {
        // 构造 TreeMap(签名通常要求参数有序)
        var params = TreeMap.$new();
        params.put("app_id", "10086");
        params.put("timestamp", String(Math.floor(Date.now() / 1000)));
        params.put("version", "2.0");
        params.put("device_id", "frida_device_001");
        params.put("platform", "android");
        
        // 构造排除签名的 key 列表
        var excludeKeys = ArrayList.$new();
        excludeKeys.add("sign");
        excludeKeys.add("_sig");
        
        // 调用签名函数
        var signature = SignUtil.sign(params, excludeKeys);
        console.log("[+] 签名结果: " + signature);
        
        return signature;
    }
    
    generateSignature();
});

参数类型转换技巧

在 Frida 中构造 Java 参数时,JavaScript 和 Java 之间的类型转换是一个常见坑点。

基本类型 vs 包装类型

Java 有基本类型(intlongboolean)和包装类型(IntegerLongBoolean)。Frida 在调用方法时会自动进行部分转换,但有时需要手动处理:

var Integer = Java.use('java.lang.Integer');
var Long = Java.use('java.lang.Long');

// Frida 会自动将 JavaScript number 转为 Java int/long
someMethod(42, 1234567890123);

// 但如果方法参数类型是包装类型 Integer(而非 int),建议显式构造
var intObj = Integer.valueOf(42);
someWrapperMethod(intObj);

// 大数需要用 Long
var longVal = Long.valueOf(9999999999);
someLongMethod(longVal);

byte 数组的十六进制转换

在加密场景中,经常需要在 byte 数组和十六进制字符串之间转换:

// byte[] 转 hex 字符串
function bytesToHex(bytes) {
    var hex = "";
    for (var i = 0; i < bytes.length; i++) {
        var b = (bytes[i] & 0xff).toString(16);
        hex += (b.length === 1 ? "0" + b : b);
    }
    return hex;
}

// hex 字符串转 byte[]
function hexToBytes(hexStr) {
    var bytes = [];
    for (var i = 0; i < hexStr.length; i += 2) {
        bytes.push(parseInt(hexStr.substr(i, 2), 16));
    }
    return Java.array('byte', bytes);
}

// 使用示例
var keyHex = "31323334353637383930313233343536";
var keyBytes = hexToBytes(keyHex);
console.log("Key hex: " + bytesToHex(keyBytes));

字符串编码处理

var StringClass = Java.use('java.lang.String');

// Java String 转 UTF-8 byte[]
var str = "中文测试";
var utf8Bytes = Java.array('byte', str.getBytes("UTF-8"));

// byte[] 转 Java String
var decoded = StringClass.$new.overload('[B', 'java.lang.String').$new(utf8Bytes, "UTF-8");

常见错误和调试方法

1. ClassNotFoundException

错误Error: java.lang.ClassNotFoundException: com.example.MyClass

原因:目标类尚未被 ClassLoader 加载。

解决:在合适的时机(如 Activity 启动后)执行脚本,或使用 Java.enumerateClassLoaders() 手动切换 ClassLoader。

2. WrongMethodType / NoSuchMethod

错误Error: no such method: $new()

原因:构造函数参数不匹配,或类没有无参构造函数。

解决:使用 overload() 显式指定参数类型,或先查看类有哪些构造函数:

var Target = Java.use('com.example.Target');
// 打印所有构造函数
Target.$init.overloads.forEach(function (overload) {
    console.log("Constructor: " + overload.argumentTypes.map(function (t) { return t.className; }).join(', '));
});

3. 类型不匹配

错误Error: expected ... but got ...

原因:JavaScript 的 number 类型与 Java 的 long 类型不匹配。

解决:使用 Java.use('java.lang.Long').valueOf() 显式构造。

4. 空指针异常

错误Error: java.lang.NullPointerException

原因:构造的对象某个方法返回了 null,但在 JavaScript 中被当作对象使用。

解决:增加 null 检查:

var result = someMethod();
if (result !== null) {
    console.log(result.toString());
} else {
    console.log("[!] 返回值为 null");
}

调试技巧

使用 Frida 的 Java.available()try-catch 包裹关键操作,确保脚本不会因单个错误而崩溃:

Java.perform(function () {
    try {
        var Target = Java.use('com.example.Target');
        var instance = Target.$new();
        // ... 你的逻辑
    } catch (e) {
        console.log("[-] Error: " + e);
        console.log("    Stack: " + e.stack);
    }
});

同时建议在关键步骤添加日志输出,方便追踪参数构造过程中的问题:

function debugObject(obj, label) {
    if (obj === null) {
        console.log("[DEBUG] " + label + " = null");
    } else {
        console.log("[DEBUG] " + label + " = " + obj);
        console.log("[DEBUG] " + label + " type = " + obj.getClass().getName());
    }
}

掌握了这些参数构造技巧后,你就能在 Frida 中自由地调用几乎任何 Java 方法,无论是分析加密算法、伪造 API 请求签名,还是模拟内部函数调用,都将成为得心应手的操作。